艾草:「もうだめだ。我已經沒有梗了,不行了...」
「艾草,醒醒!我們不是說好要一起征服這個世界嗎?」
艾草:「我..的魔法作業都做不完啦,嗚哇啊啊啊,救我~~」
「...上次的古書好像有提到讓人充滿動力的魔法 - todo 實作魔法!魔法咒語 ㄉㄡ ㄌ ㄟ ㄇ一ㄙ ㄡ ~~」
艾草:「等一下,先不要 todo 只是拿來壓時程的東西啊啊, No ~ 身體自己動起來了!」
(艾草壞掉中)
此次使用六角學院提供於「Vtuber x Coding 蹦出新滋味 ⚙️」影片下方的 todoList 版型,並參考影片內容實作 todolist !
功能切分如下:
input
細節優化首先附上 HTML 程式碼:
<div class="container">
<h1>TODO LIST</h1>
<div class="card input">
<input type="text" placeholder="請輸入待辦事項" id="inputVal" />
<a href="#" class="btn_add" id="addTodoBtn">+</a>
</div>
<div class="card card_list">
<ul class="tab" id="tab">
<li class="active" data-tab="all">全部</li>
<li data-tab="work">待完成</li>
<li data-tab="done">已完成</li>
</ul>
<div class="cart_content">
<ul class="list" id="todoList">
</ul>
<div class="list_footer">
<p><span id="workNum"></span> 個待完成項目</p>
<a href="#" id="deleteBTN">清除已完成項目</a>
</div>
</div>
</div>
</div>
最先開始實作的功能為新增待辦事項,實作過程基本如下:
input
欄位輸入的值new Date().getTime()
)input
欄位是否有值,有值的情況下,將內容推到全域變數的陣列內forEach
組字串,字串須留意要埋藏checked
屬性至 input
欄位(判斷完成狀態)input
文字內容innerHTML
渲染至網頁上,並執行該函式//透過 querySelector 選取 input 欄位
const inputVal = document.querySelector("#inputVal");
//透過 querySelector 選取 button 欄位(新增按鈕)
const addTodoBtn = document.querySelector("#addTodoBtn");
//宣告全域變數 todoData 來接組出的物件資料
let todoData = [];
//監聽是否點擊新增按鈕
addTodoBtn.addEventListener("click", addTodo);
//一點擊就執行 addTodo()
function addTodo() {
// 組出未來要用到的物件
let todo = {
// input 的值
txt: inputVal.value,
// id 用 getTime() 取毫秒
id: new Date().getTime(),
//紀錄待辦事項完成狀態
complete: false
};
//防呆 確保有填入文字
if (todo.txt.trim() !== "") {
//要塞在第一筆資料,所以用 unshift 把組好的 todo 物件賦予到外層的 todoData
todoData.unshift(todo);
// 把 input 欄位清空
inputVal.value = ""; //清空
}
//跑 render 函式,把外層的 todoData 放進去
render(todoData);
}
//透過 querySelector 選取要放入資料的 ul
const todoList = document.querySelector("#todoList");
//渲染的函式
function render(todo) {
let str = "";
//透過 todoData 跑迴圈
todo.forEach((item) => {
//將 todo 的 id 透過 data-id 埋進去
//將是否打勾埋在 input 標籤內
//將字放進去
str += `<li data-id="${item.id}">
<label class="checkbox" for="">
<input type="checkbox" ${item.complete ? "checked" : ""}/>
<span>${item.txt}</span>
</label>
<a href="#" class="delete"></a>
</li>`;
});
//最後 innerHTML 把組好的字串賦予給 todoList
todoList.innerHTML = str;
}
補充:
Date
物件:基於世界標準時間(UTC) 1970 年 1 月 1 日開始的毫秒數值來儲存時間,可以透過 new Date().getTime()
的方式來當成 id 使用。data-"自定義名稱"
:可以拿來埋各種資料進 HTML 結構內。checked
屬性:input
checkbox
的屬性 checked
,可以使 checkbox
維持打勾執行監聽 ul
區域內的刪除功能與打勾時完成狀態能切換。
實作流程:
ul
區塊的點擊事件li
的 id 值input
欄位,要透過 closest
才能點擊到 li
li
取出的 id 值字串型別轉型為數字型別findIndex
比對符合的 id 後使用陣列方法刪除該筆資料forEach
比對點擊 id 是否符合 Data
內的 id 值true/false
//監聽註冊 ul todoList 的點擊事件
todoList.addEventListener("click", (e) => {
//透過 closest 的方式能找出點擊到的 li 標籤
//透過 dataset.id 取出埋在該 li 內的 id
//取出來的 id 會是字串型別記得幫它轉型成數字型別
let id = parseInt(e.target.closest("li").dataset.id);
//刪除功能
//透過 nodeName 確認是否為 A 連結
if (e.target.nodeName === "A") {
e.preventDefault(); //取消 a 標籤預設行為
//透過陣法方法 findIndex 比對 todoData 內的 id 是否等於點擊到的 id
let index = todoData.findIndex((item) => item.id === id);
//如果是的話刪除該筆資料
todoData.splice(index, 1);
} else {
//切換打勾功能
//透過 todoData 去跑 forEach
todoData.forEach((item) => {
//如果 todoData 內的 id 是否等於點擊到的 id
if (item.id === id) {
//更改資料是否狀態
item.complete ? (item.complete = false) : (item.complete = true);
}
});
}
//重新渲染
render(todoData);
});
補充:
closest
:當在複雜 HTML 結構中想透過 e.target
選取某個 Element
,卻都只能選到它的子層時,可以透過 closest
去取到自己想要的父層 Element
。接下來實作點擊此區需有狀態樣式切換,並能篩選出對應資料。
實作流程:
tab
內新增 class
名稱為 active
實現
tab
區塊status
記錄 tab
點擊狀態並取出該 HTML 結構內埋藏的值,預設為 all
querySelectorAll
選取所有 tabs 狀態forEach
先移除所有 tabs
active
樣式tab
新增 active
樣式透過 status
狀態判斷點擊到的狀態為何,並將資料賦予給全域儲存待辦事項陣列的變數
a. 為 all
全部時顯示全域儲存變數
b. 為 work
待完成時篩選出未完成狀態
c. 都不是時,篩選出已完成狀態
透過未完成狀態長度篩選出左下角待完成項目,並渲染至網頁上
將更新後的資料透過渲染函式執行
重要:透過 updateList()
函式取代掉其它地方之 render(todo)
渲染函式呼叫
//切換 tab
//透過 querySelector 選取 id tab
const tab = document.querySelector("#tab");
//預設顯示狀態為全部
let status = "all";
//註冊監聽是否點擊到 tab
tab.addEventListener("click", changeTab);
//點擊到 tab 就執行 changeTab(e)
function changeTab(e) {
//透過 e.target 將 dataset 埋入的 tab 取出
status = e.target.dataset.tab;
//透過 querySelectorAll 選取 tab 標籤底下的 li
let tabs = document.querySelectorAll("#tab li"); //類陣列
//點擊時 tab 先清掉全部 class 樣式
tabs.forEach((item) => {
//先移除全部的 class active 樣式
item.setAttribute("class", "");
});
//有被點擊到的才加 class 樣式
e.target.setAttribute("class", "active");
//切換頁面重新渲染
updateList();
}
//修改完成狀態
function updateList() {
//切換不同頁面顯示資料
let showData = [];
//跟切換 tab 的 status 整合
if (status === "all") {
//狀態為全部 "all" 時就全部顯示
showData = todoData;
//狀態為待完成 "work" 時
} else if (status === "work") {
//篩選出未完成
showData = todoData.filter((item) => !item.complete);
} else {
//篩選出已完成
showData = todoData.filter((item) => item.complete);
}
//計算幾個待完成項目 (左下角)
const workNum = document.querySelector("#workNum");
//篩選出未完成的長度
let todoLength = todoData.filter((item) => !item.complete);
//並將長度賦予到該 DOM 節點上
workNum.textContent = todoLength.length;
//渲染 showData
render(showData);
}
updateList(); //初始化頁面
此階段執行清除所有已完成項目,並優化新增功能,使用 enter 按鍵也能新增待辦事項。
實作流程:
input
欄位的鍵盤事件程式碼:
// 清除已完成項目
// 透過 querySelector 選取 id 為 deleteBTN 的 DOM
const deleteBTN = document.querySelector("#deleteBTN");
// 註冊監聽 deleteBTN 的點擊事件
deleteBTN.addEventListener("click", function (e) {
//取消預設效果
e.preventDefault();
//重新將 todoData 賦予未完成的資料
todoData = todoData.filter((item) => !item.complete);
//重新渲染 updateList()
updateList();
});
//點擊 Enter 也可以新增資料
//註冊監聽 inputVal 的鍵盤 "keyup" 事件
inputVal.addEventListener("keyup", function (e) {
//如果點擊到 "Enter"
if (e.key === "Enter") {
//執行新增該筆資料
addTodo();
}
});
補充:
keyup
:鍵盤事件 keyup
可以拿來偵測是否按下特定鍵盤,而 keyup
的觸發時機為當你按下特定鍵盤又放開的那刻。todoList 可以練習新增、切換狀態、刪除功能,後續還可以持續優化新增編輯功能,透過練習 todoList 更了解 JavaScript 魔法!